﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Net;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Windows.Forms;

namespace KrileUpdater
{
    public class UpdateProcessor
    {
        Action<string> writer;
        public UpdateProcessor(Action<string> writer)
        {
            this.writer = writer;
        }

        private void Write(string txt)
        {
            writer.Invoke(txt);
        }

        private void WriteLine(string txt)
        {
            this.Write(txt + Environment.NewLine);
        }

        public string GetCallbackFile()
        {
            return "krile.exe";
        }

        public string UpdaterVersion = "v2.10";
        public string[] GetPatchUrls(double orig)
        {
            List<string> requires = new List<string>();
            if (CheckUnder(orig, 0.903))
            {
                CleanupDirectory(new[] { "plugins" }, new[] { "twirile.*" }, false);
                CleanupDirectory(
                    new[] { "", "plugins", "ja-JP", "plugins\\ja-JP" },
                    new[] { "*.xml", "krile.state" },
                    true);
            }

            if (CheckUnder(orig, 0.921))
            {
                CleanupDirectory(new[] { "plugins" }, new[] { "krile.common.dll" }, false);
            }
            if (CheckUnder(orig, 0.93))
            {
                CleanupDirectory(new[] { "plugins" }, new[] { "license.txt" }, false);
            }

            if (CheckUnder(orig, 0.95))
            {
                var mig = new Migrate._095.Migrator();
                WriteLine(mig.MigrateState(AppDomain.CurrentDomain.BaseDirectory));
            }

            if (CheckUnder(orig, 1.2))
                requires.Add("http://update.starwing.net/ku/120.kua");

            return requires.ToArray();
        }

        /// <summary>
        /// ディレクトリを指定したフィルタで捜索し、削除します。
        /// </summary>
        /// <param name="target">対象ディレクトリ配列(相対)</param>
        /// <param name="searchOption">検索対象フィルタ</param>
        /// <param name="unhitToDelete">ヒットしない場合に削除</param>
        private void CleanupDirectory(string[] target, string[] filters, bool unhitToDelete)
        {
            if (unhitToDelete)
            {
                foreach (var t in target)
                {
                    var pdir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, t);
                    if (Directory.Exists(pdir))
                    {
                        WriteLine("directory clean-up:[target]" + t + "[filter]<!>" + String.Join(";", filters));
                        var dirfiles = Directory.GetFiles(pdir);
                        HashSet<string> filtered = new HashSet<string>();
                        foreach (var filter in filters)
                        {
                            var dfs = Directory.GetFiles(pdir, filter);
                            if (dfs != null)
                            {
                                foreach (var s in dfs)
                                    filtered.Add(s);
                            }
                        }
                        foreach (var f in dirfiles.Except(filtered))
                        {
                            try
                            {
                                File.Delete(f);
                            }
                            catch (Exception e)
                            {
                                WriteLine(" <!> Delete error:" + e.Message);
                            }
                        }
                    }
                }
            }
            else
            {
                foreach (var t in target)
                {
                    var pdir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, t);
                    if (Directory.Exists(pdir))
                    {
                        WriteLine("directory clean-up:[target]" + t + "[filter]" + String.Join(";", filters));
                        foreach (var filter in filters)
                        {
                            foreach (var f in Directory.GetFiles(pdir, filter))
                            {
                                try
                                {
                                    File.Delete(f);
                                }
                                catch (Exception e)
                                {
                                    WriteLine(" <!> Delete error:" + e.Message);
                                }
                            }
                        }
                    }
                }
            }
        }

        private bool CheckUnder(double orig, double curv)
        {
            DestinationVer = curv;
            return orig < curv;
        }

        public string Destination = String.Empty;

        public double DestinationVer = 0.0;

        public void DoUpdate(double originateVersion)
        {
            SetCancellableInvoke(true);
            WriteLine("Loading patch files information...");
            var cand = GetPatchUrls(originateVersion);
            WriteLine("This utility updates your Krile from v" + originateVersion.ToString("0.000") + " to v" + DestinationVer.ToString("0.000") + ".");
            List<string> patchdlpath = new List<string>();
            int ctor = 1;
            WriteLine("Required:" + cand.Length + " patches");
            foreach (var c in cand)
            {
                var tf = Path.GetTempFileName();
                patchdlpath.Add(tf);
                for (int i = 0; i < 10; i++)
                {
                    Write("Downloading patches pack " + ctor.ToString() + " from " + c + "...");
                    if (DownloadFiles(new Uri(c), tf))
                        break;
                    WriteLine("Failed!:Auto-retry in 3 seconds...");
                    System.Threading.Thread.Sleep(3000);
                }
                WriteLine("done.");
                ctor++;
            }
            Write("Starting patching...");
            for (int i = 0; i < 10; i++)
            {
                Write(".");
                for (int j = 0; j < 20; j++)
                    System.Threading.Thread.Sleep(10);
            }

            SetCancellableInvoke(false);
            ctor = 1;
            foreach (var p in patchdlpath)
            {
                string dest = null;
                while (dest == null || Directory.Exists(dest))
                {
                    dest = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
                }
                Directory.CreateDirectory(dest);
                WriteLine("Patch " + ctor.ToString() + ":");
                Write("Unpacking patches pack...");
                DecompressFile(p, dest);
                WriteLine("done.");
                Write("Patching files...");
                MoveDirectory(dest, Destination, true);
                WriteLine("done.");
                Write("Finalizing...");
                Directory.Delete(dest, true);
                File.Delete(p);
                WriteLine("done.");
                ctor++;
            }

            WriteLine("");
            WriteLine("Update completed!");
        }

        private bool DownloadFiles(Uri uri, string dest)
        {
            try
            {
                WebRequest req = HttpWebRequest.Create(uri);
                req.Proxy = HttpWebRequest.DefaultWebProxy;
                var ret = req.GetResponse();
                if (ret != null)
                {
                    using (var strm = ret.GetResponseStream())
                    using(var outfile = new FileStream(dest,  FileMode.Create, FileAccess.Write))
                    {
                        byte[] buf = new byte[1024];
                        while (true)
                        {
                            int rlen = strm.Read(buf, 0, buf.Length);
                            if (rlen == 0) break;
                            outfile.Write(buf, 0, rlen);
                        }
                    }
                }
            }
            catch (WebException e)
            {
                WriteLine("Download error:" + e.ToString());
                return false;
            }
            var hash = GetFileHash(dest);
            try
            {
                WebRequest req = HttpWebRequest.Create(uri + ".md5");
                var ret = req.GetResponse();
                if (ret != null)
                {
                    using (var sr = new StreamReader(ret.GetResponseStream()))
                    {
                        var orghash = sr.ReadToEnd();
                        if (orghash == hash)
                            return true;
                        else
                        {
                            WriteLine("Download error:File mismatched.");
                            return false;
                        }
                    }
                }
            }
            catch (WebException e)
            {
                WriteLine("Download error:" + e.ToString());
                return false;
            }
            return false;
        }

        private void DecompressFile(string archive, string extractDir)
        {
            using (var fs = new FileStream(archive, FileMode.Open, FileAccess.Read))
            {
                using (var gz = new GZipStream(fs, CompressionMode.Decompress))
                {
                    byte[] buffer = new byte[1024];
                    List<FileData> fd = new List<FileData>();
                    if (gz.Read(buffer, 0, 256) < 256)
                        throw new Exception("Downloaded patch's format was invalid! (Length error)");
                    var infostr = Encoding.UTF8.GetString(buffer).Split(new[] { ":" }, StringSplitOptions.None);
                    if (infostr.Length < 3)
                        throw new Exception("Downloaded patch was corrupted!");
                    if (infostr[0] != "KUA")
                        throw new Exception("Downloaded patch was not supported!");
                    int arcnum = int.Parse(infostr[1]);
                    long desclen = long.Parse(infostr[2]);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        ReadStream(gz, ms, desclen);
                        var fds = Encoding.UTF8.GetString(ms.ToArray()).Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                        foreach (var f in fds)
                        {
                            var sfd = f.Split(new[] { "?" }, StringSplitOptions.None);
                            if (sfd.Length != 3)
                                throw new Exception("Downloaded patch's format was invalid! (Splitting error)");
                            fd.Add(new FileData() { name = sfd[0], length = long.Parse(sfd[2]), checksum = sfd[1] });
                        }
                        if (fd.Count != arcnum)
                            throw new Exception("Losted some files!");

                        foreach (var fdi in fd)
                        {
                            var tgfp = Path.Combine(extractDir, fdi.name);
                            if (!Directory.Exists(Path.GetDirectoryName(tgfp)))
                            {
                                Directory.CreateDirectory(Path.GetDirectoryName(tgfp));
                            }
                            using (var cfs = new FileStream(tgfp, FileMode.Create, FileAccess.ReadWrite))
                            {
                                ReadStream(gz, cfs, fdi.length);
                            }
                            if (GetFileHash(tgfp) != fdi.checksum)
                                throw new Exception("Downloaded patch's hash was not matched!");
                        }
                    }
                }
            }
        }

        public void ReadStream(Stream ins, Stream outs, long length)
        {
            byte[] buffer = new byte[1024];
            long readedLen = 0;
            while (true)
            {
                long nextRead = length - readedLen;
                if (nextRead > 1024)
                    nextRead = 1024;
                //書庫から展開されたデータを読み込む
                int readSize = ins.Read(buffer, 0, (int)nextRead);
                readedLen += readSize;
                //最後まで読み込んだ時は、ループを抜ける
                if (readSize == 0)
                    return;
                //出力に書き込む
                outs.Write(buffer, 0, readSize);
                if (readedLen >= length)
                    return;
            }
        }

        public string GetFileHash(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var md5 = new MD5CryptoServiceProvider();
                try
                {
                    var ret = md5.ComputeHash(fs);
                    return BitConverter.ToString(ret).ToLower().Replace("-", "");
                }
                finally
                {
                    md5.Clear();
                }
            }
        }

        public void MoveDirectory(string source, string dest, bool overwritable)
        {
            if (!Directory.Exists(dest))
            {
                Directory.CreateDirectory(dest);
                File.SetAttributes(dest, File.GetAttributes(source));
                overwritable = true;
            }

            foreach (string cfrom in Directory.GetFiles(source))
            {
                string cdest = Path.Combine(dest, Path.GetFileName(cfrom));

                if (File.Exists(cdest))
                {
                    if (overwritable)
                    {
                    FileDelete:
                        try
                        {
                            File.Delete(cdest);
                        }
                        catch (IOException)
                        {
                            var ret = MessageBox.Show(
                                "File delete error!" + Environment.NewLine +
                                "This file can't remove:" + cdest,
                                "Write error", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error);
                            switch (ret)
                            {
                                case DialogResult.Ignore:
                                    break;
                                case DialogResult.Retry:
                                    goto FileDelete;
                                case DialogResult.Abort:
                                    throw new Exception("Aborted.");
                            }
                        }
                    }
                    else
                    {
                        continue;
                    }
                }
                File.Move(cfrom, cdest);
            }

            //recursive
            foreach (string copyFrom in Directory.GetDirectories(source))
            {
                string copyTo = Path.Combine(dest, Path.GetFileName(copyFrom));
                MoveDirectory(copyFrom, copyTo, overwritable);
            }
        }

        public event Action<bool> SetCancellable;
        private void SetCancellableInvoke(bool cancellable)
        {
            if (SetCancellable != null)
                SetCancellable.Invoke(cancellable);
        }

        public class FileData
        {
            public string name;
            public long length;
            public string checksum;
        }

    }
}
